/*
 * Object   : CServer
 * Type     : Backend
 *
 * Narrator : Kamran H. Khan 
 * Email    : KAMRAN.KHAN@MaximaLabssolutions.com, KAMRANHK@gmail.com
 * 
 * Copyright :
 *            MaximaLabs Solutions (R) 2009
 * 
 * 
 * Description: 
 * Serves a base class for server objects
 * 
 * Revision History:
 * 
 * Date(MMM/dd/yy)      Owner           Description
 * ----------------------------------------------------------------------------------------------------------------------------------------------
 * May/30/09            Kamran H. Khan  Added skeleton code
 * May/30/09            Kamran H. Khan  Added more methods
 * May/30/09            Kamran H. Khan  Added Listen()
 *                                      -Send(), Start(), Stop()
 *                                      -StatusChange() event handler
 *                                      -Added ManageClient(), 
 *                                      -added threading pool management.
 * 
*/


using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Net;
using System.ComponentModel;
using System.Diagnostics;
using Maxima.Common;
using System.Collections.Concurrent;


namespace Maxima.Comlib
{
    public class CServer : CNetMgr, IDisposable
    {
        public TcpListener m_theSvr = null; //the main server object
        private bool m_bStopRequested;
        Thread m_threadConnectionMgr = null;
        //CProcessor m_theProcessor;

        private bool m_bRunning = false;
        public bool IsConnected
        {
            get { return m_bRunning; }
        }

        //private string m_strIP;
        //[Category("Settings")]
        //[Description("IP Address")]
        //[BrowsableAttribute(true)]
        //public string IP
        //{
        //    get { return m_strIP; }
        //    set { m_strIP = value; }
        //}

        //private int m_nPort;

        //[Category("Settings")]
        //[Description("Port")]
        //[BrowsableAttribute(true)]
        //public int Port
        //{
        //    get { return m_nPort; }
        //    set { m_nPort = value; }
        //}

        public string m_strStatus;
        private readonly System.Collections.Concurrent.ConcurrentDictionary<string, TcpClient> _clients = new System.Collections.Concurrent.ConcurrentDictionary<string, TcpClient>();

        private List<TcpClient> _lstClients = new List<TcpClient>();

        [Category("Settings")]
        [Description("Provides the list of connected clients")]
        [BrowsableAttribute(true)]
        public List<TcpClient> ListOfClients
        {
            get { return _lstClients; }
        }

        public delegate void StatusChangeEventHandler(string strData);//Custom events to propogate to user interface 
        public event StatusChangeEventHandler OnStatusChange;


        /// <summary>
        /// Initializes a new instance of the <see cref="CServer"/> class.
        /// </summary>
        /// <param name="strIP">The STR IP.</param>
        /// <param name="nPort">The n port.</param>
        public CServer(string strIP, int nPort)
            : base(strIP, nPort, false)
        {
            LogEx.Info("Initialization success.");

            PropagateMessageToUpperLayer("Loading server...");


        }

        public CServer()
            : base()
        {
            LogEx.Info("Initialization success.");

            //PropagateMessageToUpperLayer("Loading server...");
            try
            {
                LogEx.Info("Trying to listen on (IP:Port): [" + base.IP + ":" + base.Port.ToString() + "]");
                //m_theSvr = new TcpListener(IPAddress.Parse(base.IP), Convert.ToInt32(base.Port));
                //m_theSvr = new TcpListener(base.Port);
                m_theSvr = new TcpListener(IPAddress.Any, base.Port);
            }
            catch (Exception ex)
            {
                LogEx.Error("Init failed", ex);
            }

        }

        /// <summary>
        /// Starts this instance.
        /// </summary>
        public override void Connect()
        {
            //StartThread();
            //m_theProcessor.Init();
            //m_theProcessor = new CProcessor();//Initialise the main processor object

            LogEx.Info("Inside method.");


            m_bStopRequested = false;

            m_threadConnectionMgr = new Thread(delegate() { ListenIncomingConnections(); });
            m_threadConnectionMgr.IsBackground = true;
            m_threadConnectionMgr.Start();

            PropagateMessageToUpperLayer("Server started.");
            LogEx.Info("End.");
        }

        /// <summary>
        /// Listens this instance.
        /// </summary>
        private void ListenIncomingConnections()
        {
            LogEx.Info("Inside method.");

            if (m_theSvr == null)
                return;

            try
            {
                //m_theSvr = new TcpListener(IPAddress.Parse(base.IP), Convert.ToInt32(base.Port));
                m_theSvr.Start(Constants.NO_OF_MAX_CLIENTS_SUPPORTED); //the backlog, for now its 10
                m_bRunning = true;
                LogEx.Info("Started");

                while (!m_bStopRequested)
                {
                    //PropagateMessageToUpperLayer("Listening...");
                    LogEx.Info("Waiting for new connection...");

                    if (m_theSvr == null)
                        break;

                    TcpClient newConnection = m_theSvr.AcceptTcpClient();

                    if (!IsValidClient(newConnection))
                    {
                        //CLogEx.Info( "Error: " + RxCodes.);;

                        LogEx.Error("Invalid connection, rejected :[" + newConnection.Client.RemoteEndPoint.ToString() + "]");
                        newConnection.Client.Close();
                        newConnection.Close();
                        newConnection = null;

                    }
                    else
                    {
                        //PropagateMessageToUpperLayer(newConnection.Client.LocalEndPoint.ToString() + ">>New entry... : [" + newConnection.Client.LocalEndPoint.ToString() + "]");

                        LogEx.Info("New connection : [" + newConnection.Client.RemoteEndPoint.ToString() + "]");

                        //todo: add max limit check.
                        _lstClients.Add(newConnection);

                        var id = Guid.NewGuid().ToString();
                        if (_clients.TryAdd(id, newConnection)) continue;// new LocalClient(client, ReceivedMessage, ClientClosed, id))) continue;



                        LogEx.Info("Added to list.");
                        LogEx.Info("Concurrent connections: [" + _lstClients.Count.ToString() + "]");
                        Thread thdManageClient = new Thread(new ParameterizedThreadStart(ManageClientEx));
                        thdManageClient.IsBackground = true;
                        thdManageClient.Start(newConnection);

                        LogEx.Info("Thread spawned.");
                    }
                }
            }
            catch (SocketException ex)
            {
                LogEx.Error("Unable to listen", ex);
            }
            catch (Exception ex)
            {
                LogEx.Error("Unable to listen", ex);
            }
            finally
            {
                m_theSvr.Stop();
            }

            LogEx.Info("End.");
        }

        private bool IsValidClient(TcpClient thisClient)
        {
            return true;// consider all connections valid.
            //TODO: make a whitelist, for clean-clients to grant connections.
        }

        private void ManageClientEx(object theClient)
        {
            LogEx.Info("Inside method");
            TcpClient thisClient = (TcpClient)theClient;
            LogEx.Info("Connected: [" + thisClient.Client.RemoteEndPoint.ToString() + "]");

            bool bShouldDisconnect = false;
            int nMessageCount = 0;

            //2014-Sep-08: Part of the protocol, for now, just to start with; meta length
            int nHeaderBytes = Constants.HEADER_BYTES;// 4;//should come from configuration.

            while (true)
            {
                try
                {
                    LogEx.Info("Waiting for data");

                    string strHeaderBytes = ReadData(thisClient.Client, //the client
                                                     nHeaderBytes, // bytes to be read
                                                     out bShouldDisconnect);//Invalid header bytes? disconnect this client.

                    LogEx.Info("Read bytes: [" + strHeaderBytes + "]");
                    if (bShouldDisconnect)
                    {
                        //Disconnect the client, if invalid header bytes.
                        _lstClients.Remove(thisClient); thisClient.Close();
                        break;
                    }
                    else
                    {

                        int nDataReceived = 0;
                        string data = string.Empty;
                        bool bReadable = int.TryParse(strHeaderBytes.ToString(), out nDataReceived);
                        if (!bReadable || nDataReceived < 0)
                        {
                            //Disconnect the client, if invalid header bytes.
                            LogEx.Error("Invalid header bytes: [" + strHeaderBytes + "]");
                            LogEx.Info("Kicking out: [" + thisClient.Client.RemoteEndPoint.ToString() + "] ");
                            _lstClients.Remove(thisClient);
                            thisClient.Close();
                            LogEx.Info("Concurrent connections: [" + _lstClients.Count.ToString() + "]");
                            break;
                        }
                        else if (nDataReceived == 0)
                        {
                            //Echo if empty message.
                            LogEx.Info("0 bytes recieved; assuming a disconnect signal.");
                            LogEx.Info("Sending disconnect acknowledge signal; echo 0 bytes.");

                            //todo: change ascii encoding to binary, better, multi platform support, easy to maintain.
                            int nBytesSent = thisClient.Client.Send(System.Text.ASCIIEncoding.ASCII.GetBytes("0"));

                            LogEx.Info("Disconnecting:[" + thisClient.Client.RemoteEndPoint.ToString() + "] ");
                            _lstClients.Remove(thisClient);
                            LogEx.Info("Concurrent connections: [" + _lstClients.Count.ToString() + "]");

                            thisClient.Close();
                            break;
                        }
                        else
                        {
                            //1. Delegate the incoming message to CSimHandler
                            //2. Have it process the message, prepare the corresponding response string
                            //3. Respond to client.
                            data = ReadData(thisClient.Client, nDataReceived, out bShouldDisconnect);

                            //Throw to UI layer for processing.
                            //PropagateMessageToUpperLayer(strRawMsg);//Btw, a not so good design. Should only be used if dealing with UI, otherwise push into some queue, process the message, and respond to client.

                            LogEx.Info("Rx: [{0}]", data);
                            int nBytesSent = thisClient.Client.Send(System.Text.ASCIIEncoding.ASCII.GetBytes(data.Length.ToString().PadLeft(4, '0').ToString() + data));
                            LogEx.Info("Sent to: " + thisClient.Client.RemoteEndPoint.ToString());

                            //Btw, worst class design.
                            //TODO: Revamp
                            //Solution 1: Producer/consumer architecture, push incoming messages into a queue; process, translate message, and respond. Prosposed, quality design.
                            //Solution 2: Integrate with the CSimHandler class, not a good design anyway.
                        }
                    }

                }
                catch (Exception ex)
                {
                    LogEx.Error("Non-compliant message format; bad client.", ex);
                    LogEx.Error("Kicking out:[" + thisClient.Client.RemoteEndPoint.ToString() + "] ");
                    thisClient.Close();
                    break;
                }
            }

            LogEx.Info("Messages processed for this channel: [" + nMessageCount.ToString() + "]");
            LogEx.Info("End");
        }



        string ReadData(Socket sckClient, int nBytesToRead, out bool bShouldDisconnect)
        {
            LogEx.Info("Inside");

            bShouldDisconnect = false;

            byte[] byteBuffer = new byte[nBytesToRead];
            Array.Clear(byteBuffer, 0, byteBuffer.Length);

            int nDataRead = 0;
            int nStartIndex = 0;
            int nBytesRead = 0;
            //while (nDataRead <= nBytesToRead)
            while (nDataRead < nBytesToRead)
            {
                try
                {
                    nBytesRead = sckClient.Receive(byteBuffer, nStartIndex, nBytesToRead - nStartIndex, SocketFlags.None);
                }
                catch (SocketException ex)
                {
                    LogEx.Error("What went wrong? ", ex);

                }
                if (0 == nBytesRead)
                {
                    bShouldDisconnect = true;
                    LogEx.Info("0 bytes received; assuming disconnect signal");
                    break;
                }

                nDataRead += nBytesRead;
                nStartIndex += nBytesRead;
                LogEx.Info("Data : [" + Encoding.Default.GetString(byteBuffer, 0, nDataRead) + "]");
                LogEx.Info("Bytes read: " + nBytesRead.ToString());
                LogEx.Info("Bytes remaining: " + (nBytesToRead - nDataRead).ToString());
            }

            return Encoding.Default.GetString(byteBuffer, 0, nDataRead);
        }


        /// <summary>
        /// Manages the client.
        /// </summary>
        /// <param name="theClient">The client.</param>
        [Obsolete("Deffered; use ManageClientEx() instead", true)]
        private void ManageClient(object theClient)
        {
            LogEx.Info("Inside method");

            TcpClient thisClient = (TcpClient)theClient;
            NetworkStream theCommLayer = thisClient.GetStream();

            LogEx.Info("Connected. Managing client (IP:Port): [" + thisClient.Client.RemoteEndPoint.ToString() + "]");

            int nBytesRead = 0;
            ASCIIEncoding theEncoding = new ASCIIEncoding();
            while (true)
            {
                try
                {
                    byte[] bytMessage = new byte[READ_BUFFER_MAX_SIZE];

                    LogEx.Info("Waiting for data");
                    nBytesRead = theCommLayer.Read(bytMessage, 0, bytMessage.Length);
                    if (nBytesRead == 0)
                        continue;

                    string strMsg = theEncoding.GetString(bytMessage);
                    strMsg = strMsg.Trim();// Normalize();
                    LogEx.Info("RX: [" + strMsg + "]");

                    //Check if this is a logout request.
                    //this would never come.
                    if (IsCloseConnectionRequest(strMsg))
                    {
                        bool bRemoved = _lstClients.Remove(thisClient); //remove from the list
                        break;
                    }
                    else
                    {
                        //Send("SVR_ECHO:[" + strMsg+"]");
                        //strMsg = m_theProcessor.Execute(strMsg);

                        //if (m_theProcessor.ResponseCode != null)
                        {
                            // if (m_theProcessor.ResponseCode.Equals(MaximaLabs.RxCodes.INVALID_CHANNEL))
                            {
                                LogEx.Info("Kicking out:[" + thisClient.Client.RemoteEndPoint.ToString() + "] ");
                                thisClient.Close();
                                break;
                            }
                        }
                        //else
                        {
                            int nBytesSent = thisClient.Client.Send(System.Text.ASCIIEncoding.ASCII.GetBytes("SVR_ECHO:[" + strMsg + "]"));
                            LogEx.Info("Sent:[" + thisClient.Client.RemoteEndPoint.ToString() + "] " + "SVR_ECHO:[" + strMsg + "]");
                        }
                    }

                }
                catch (Exception ex)
                {
                    //Error(ex.Message);
                    LogEx.Error("Couldnt manage client", ex);
                    thisClient.Close();
                    break;
                }
            }


            LogEx.Info("End");

        }




        /// <summary>
        /// Determines whether [is close connection request] [the specified STR MSG].
        /// </summary>
        /// <param name="strMsg">The STR MSG.</param>
        /// <returns>
        /// 	<c>true</c> if [is close connection request] [the specified STR MSG]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsCloseConnectionRequest(string strMsg)
        {
            if (strMsg.StartsWith(CLOSE_CONNECTION_REQUEST_CODE))//Check if its a logout intimation.
                return true;
            else return false;
        }

        /// <summary>
        /// Logs the specified STR MSG.
        /// </summary>
        /// <param name="strMsg">The STR MSG.</param>
        private void PropagateMessageToUpperLayer(string strMsg)
        {
            //m_strStatus = strMsg;// "\r\nSVR:" + 
            ////OnStatusChange = new StatusChangeEventHandler();
            //if (OnStatusChange != null)
            //    OnStatusChange(m_strStatus);


        }


        /// <summary>
        /// Sends the specified STR data.
        /// </summary>
        /// <param name="strData">The STR data.</param>
        /// <returns></returns>
        public override int Send(string strData)
        {
            LogEx.Info("Inside method");

            //StatusChangeEvent("ERROR: " + strMsg);

            int nBytesSent = 0;
            if (m_theSvr == null)
                return nBytesSent;

            try
            {
                int nIndexStart = strData.IndexOf("[");
                int nIndexEnd = strData.IndexOf("]");
                string strMsg = strData.Substring(1, nIndexEnd - 1);

                LogEx.Info("Sending to client.");
                bool bSent = false;
                foreach (TcpClient theClient in _lstClients)
                {
                    try
                    {
                        if (!theClient.Client.RemoteEndPoint.ToString().Equals(strMsg))
                            continue;

                        if (theClient.Connected)
                        {
                            LogEx.Info("Client connected");
                            if (theClient.Client.Connected)
                            {
                                nBytesSent = theClient.Client.Send(strData.ToBinary());// System.Text.ASCIIEncoding.ASCII.GetBytes(strData));

                                LogEx.Info("Sent");

                                //PropagateMessageToUpperLayer("\r\nTX: " + " [" + theClient.Client.RemoteEndPoint.ToString() + "] " + strData);
                                bSent = true;

                                break;
                            }
                            else
                            {
                                LogEx.Info("Send failed");
                            }
                        }


                    }
                    catch (Exception ex)
                    {
                        //Error(exc.Message);
                        LogEx.Error("Error while sending data 2", ex);
                    }
                }
                if (!bSent)
                    LogEx.Info("Sent to none.");
            }
            catch (Exception ex)
            {
                //Error(exc.Message);
                LogEx.Error("Error while sending data", ex);
            }

            return nBytesSent;
        }



        /// <summary>
        /// Stops this instance.
        /// </summary>
        public override void Stop()
        {
            LogEx.Info("Inside method");

            LogEx.Info("Shutting down server");

            if (m_threadConnectionMgr != null)
            {
                LogEx.Info("Aborting thread(s)");
                m_threadConnectionMgr.Abort();
                m_threadConnectionMgr = null;

                if (m_theSvr != null)
                    m_theSvr.Stop();
                m_theSvr = null;
                LogEx.Info("Abort success.");

                if (_lstClients != null)
                {
                    _lstClients.Clear();
                }

                LogEx.Info("Memory cleaned");


            }
        }



        #region IDisposable Members

        void IDisposable.Dispose()
        {
            LogEx.Info("Inside method");

            if (m_theSvr == null)
                return;

            else
            {
                LogEx.Info("Closing");

                m_theSvr.Server.Close();

                LogEx.Info("Clearing");

                _lstClients.Clear();
                _lstClients = null;

                LogEx.Info("Close success.");
            }
            LogEx.Info("End");
        }

        #endregion
    }
}
